// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

part of dart.ui;

/// The type of a key event.
// Must match the KeyEventType enum in ui/window/key_data.h.
enum KeyEventType {
  /// The key is pressed.
  down,

  /// The key is released.
  up,

  /// The key is held, causing a repeated key input.
  repeat;

  // TODO(matanlurey): have original authors document; see https://github.com/flutter/flutter/issues/151917.
  // ignore: public_member_api_docs
  String get label {
    return switch (this) {
      down => 'Key Down',
      up => 'Key Up',
      repeat => 'Key Repeat',
    };
  }
}

/// The source device for the key event.
///
/// Not all platforms supply an accurate type.
// Must match the KeyEventDeviceType enum in ui/window/key_data.h.
enum KeyEventDeviceType {
  /// The device is a keyboard.
  keyboard,

  /// The device is a directional pad on something like a television remote
  /// control or similar.
  directionalPad,

  /// The device is a gamepad button
  gamepad,

  /// The device is a joystick button
  joystick,

  /// The device is a device connected to an HDMI bus.
  hdmi;

  // TODO(matanlurey): have original authors document; see https://github.com/flutter/flutter/issues/151917.
  // ignore: public_member_api_docs
  String get label {
    return switch (this) {
      keyboard => 'Keyboard',
      directionalPad => 'Directional Pad',
      gamepad => 'Gamepad',
      joystick => 'Joystick',
      hdmi => 'HDMI',
    };
  }
}

/// Information about a key event.
class KeyData {
  /// Creates an object that represents a key event.
  const KeyData({
    required this.timeStamp,
    required this.type,
    required this.physical,
    required this.logical,
    required this.character,
    required this.synthesized,
    this.deviceType = KeyEventDeviceType.keyboard,
  });

  /// Time of event dispatch, relative to an arbitrary timeline.
  ///
  /// For synthesized events, the [timeStamp] might not be the actual time that
  /// the key press or release happens.
  final Duration timeStamp;

  /// The type of the event.
  final KeyEventType type;

  /// Describes what type of device (keyboard, directional pad, etc.) this event
  /// originated from.
  final KeyEventDeviceType deviceType;

  /// The key code for the physical key that has changed.
  final int physical;

  /// The key code for the logical key that has changed.
  final int logical;

  /// Character input from the event.
  ///
  /// Ignored for up events.
  final String? character;

  /// If [synthesized] is true, this event does not correspond to a native
  /// event.
  ///
  /// Although most of Flutter's keyboard events are transformed from native
  /// events, some events are not based on native events, and are synthesized
  /// only to conform Flutter's key event model (as documented in the
  /// `HardwareKeyboard` class in the framework).
  ///
  /// For example, some key downs or ups might be lost when the window loses
  /// focus. Some platforms provide ways to query whether a key is being held.
  /// If the embedder detects an inconsistency between its internal record and
  /// the state returned by the system, the embedder will synthesize a
  /// corresponding event to synchronize the state without breaking the event
  /// model.
  ///
  /// As another example, macOS treats CapsLock in a special way by sending down
  /// and up events at the down of alternate presses to indicate the direction
  /// in which the lock is toggled instead of that the physical key is going. A
  /// macOS embedder should normalize the behavior by converting a native down
  /// event into a down event followed immediately by a synthesized up event,
  /// and the native up event also into a down event followed immediately by a
  /// synthesized up event.
  ///
  /// Synthesized events do not have a trustworthy [timeStamp], and should not
  /// be processed as if the key actually went down or up at the time of the
  /// callback.
  ///
  /// [KeyRepeatEvent] is never synthesized.
  final bool synthesized;

  // Returns the bits that are not included in [valueMask], shifted to the
  // right.
  //
  // For example, if the input is 0x12abcdabcd, then the result is 0x12.
  //
  // This is mostly equivalent to a right shift, resolving the problem that
  // JavaScript only support 32-bit bitwise operations and needs to use division
  // instead.
  static int _nonValueBits(int n) {
    const int valueMask = 0x000FFFFFFFF;
    // `n >> valueMaskWidth` is equivalent to `n / divisorForValueMask`.
    const int divisorForValueMask = valueMask + 1;
    const int valueMaskWidth = 32;

    // Equivalent to assert(divisorForValueMask == (1 << valueMaskWidth)).
    const int firstDivisorWidth = 28;
    assert(
      divisorForValueMask == (1 << firstDivisorWidth) * (1 << (valueMaskWidth - firstDivisorWidth)),
    );

    // JS only supports up to 2^53 - 1, therefore non-value bits can only
    // contain (maxSafeIntegerWidth - valueMaskWidth) bits.
    const int maxSafeIntegerWidth = 52;
    const int nonValueMask = (1 << (maxSafeIntegerWidth - valueMaskWidth)) - 1;

    if (identical(0, 0.0)) {
      // Detects if we are on the web.
      return (n / divisorForValueMask).floor() & nonValueMask;
    } else {
      return (n >> valueMaskWidth) & nonValueMask;
    }
  }

  String _logicalToString() {
    final String result = '0x${logical.toRadixString(16)}';
    final int planeNum = _nonValueBits(logical) & 0x0FF;
    final String planeDescription = (() {
      switch (planeNum) {
        case 0x000:
          return ' (Unicode)';
        case 0x001:
          return ' (Unprintable)';
        case 0x002:
          return ' (Flutter)';
        case 0x011:
          return ' (Android)';
        case 0x012:
          return ' (Fuchsia)';
        case 0x013:
          return ' (iOS)';
        case 0x014:
          return ' (macOS)';
        case 0x015:
          return ' (GTK)';
        case 0x016:
          return ' (Windows)';
        case 0x017:
          return ' (Web)';
        case 0x018:
          return ' (GLFW)';
      }
      return '';
    })();
    return '$result$planeDescription';
  }

  String? _escapeCharacter() {
    if (character == null) {
      return '<none>';
    }
    switch (character!) {
      case '\n':
        return r'"\n"';
      case '\t':
        return r'"\t"';
      case '\r':
        return r'"\r"';
      case '\b':
        return r'"\b"';
      case '\f':
        return r'"\f"';
      default:
        return '"$character"';
    }
  }

  String? _quotedCharCode() {
    if (character == null) {
      return '';
    }
    final Iterable<String> hexChars = character!.codeUnits.map(
      (int code) => code.toRadixString(16).padLeft(2, '0'),
    );
    return ' (0x${hexChars.join(' ')})';
  }

  @override
  String toString() {
    return 'KeyData(${type.label}, '
        'physical: 0x${physical.toRadixString(16)}, '
        'logical: ${_logicalToString()}, '
        'character: ${_escapeCharacter()}${_quotedCharCode()}'
        '${synthesized ? ', synthesized' : ''}';
  }

  /// Returns a complete textual description of the information in this object.
  String toStringFull() {
    return '$runtimeType(type: ${type.label}, '
        'deviceType: ${deviceType.label}, '
        'timeStamp: $timeStamp, '
        'physical: 0x${physical.toRadixString(16)}, '
        'logical: 0x${logical.toRadixString(16)}, '
        'character: ${_escapeCharacter()}, '
        'synthesized: $synthesized'
        ')';
  }
}
